package qj.app.host;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashSet;
import java.util.Map;

import qj.app.host.AppHost.ManagedProcess;
import qj.util.HttpUtil;
import qj.util.HttpUtil.HttpMessage;
import qj.util.DateUtil;
import qj.util.DigestUtil;
import qj.util.FileUtil;
import qj.util.NetUtil;
import qj.util.ProcessUtil;
import qj.util.PropertiesUtil;
import qj.util.SystemUtil;
import qj.util.ThreadUtil;
import qj.util.ZipUtil;
import qj.util.funct.F0;
import qj.util.funct.Fs;
import qj.util.funct.P0;
import qj.util.funct.P1;
import qj.util.funct.P2;

public class Controller {
	static String key = "mf934jf2098a3";

	boolean[] serverStopped;
	public void start(int port, final Map<String, ManagedProcess> processes) {
		if (serverStopped != null) {
			throw new RuntimeException("Server already started");
		}
		serverStopped = new boolean[] {false};
		ThreadUtil.run( NetUtil.acceptF(port, new P1<Socket>() {public void e(Socket sk) {
			// Verify command socket
			
			// Receive command
			try {
				InputStream in = sk.getInputStream();
				HttpMessage httpMessage = HttpUtil.readHttpMessage(in);
				String action = httpMessage.getParameter("action");
				
				if (!checkKey(httpMessage.getParameter("key"))) {
					System.out.println("Unaccepted key");
					return;
				}
				
				if ("deploy".equals(action)) {
					deploy(httpMessage, processes);
				} else if ("stop".equals(action)) {
					stop(httpMessage.getParameter("appId"), processes);
				} else if ("stopHost".equals(action)) {
					stopHost(processes);
				} else if ("restart".equals(action)) {
					restart(httpMessage.getParameter("appId"), processes);
				} else if ("remove".equals(action)) {
					remove(httpMessage.getParameter("appId"), processes);
				}
			} catch (IOException e1) {
				throw new RuntimeException(e1.getMessage(), e1);
			}
		}}, Fs.booleanRef(serverStopped)));
		System.out.println("Listening on port " + port);
		
		startAll(processes);
		
		try {
			SystemUtil.readLine();
			System.out.println("Shutting down AppHost (received readline)");
			stopHost(processes);
		} catch (RuntimeException e) {
			if (e.getCause() instanceof IOException) {
				while (true) {
					if (!serverStopped[0]) {
						ThreadUtil.sleep(2000);
					}
				}
			}
		}
	}

	protected void stopHost(Map<String, ManagedProcess> processes) {
		stopAll(processes);
		System.out.println("Shutdown all apps done.");
		serverStopped[0] = true;
	}

	protected static boolean checkKey(String digestedKey) {
		if (key == null) {
			return true;
		}
		
		long minute = System.currentTimeMillis()/5/DateUtil.MINUTE_LENGTH;
		if (DigestUtil.digest(key + minute).equals(digestedKey)) {
			return true;
		} else if (DigestUtil.digest(key + (minute - 1)).equals(digestedKey)) {
			return true;
		} else if (DigestUtil.digest(key + (minute + 1)).equals(digestedKey)) {
			return true;
		}
		
		return false;
	}

	private static void deploy(HttpMessage httpMessage,
			final Map<String, ManagedProcess> processes) {
		String appId = httpMessage.getParameter("appId");
		System.out.println("Received command deploy for app: " + appId);
		System.out.println("Terminating old app " + appId);
		stop(appId, processes);
		System.out.println("Deploying application " + appId);
		try {
			deploy(appId, httpMessage.getContent());
		} catch (Exception e) {
			System.out.println("Deploy failed: " + e);
		}
		exec(appId, processes);
	}
	

	protected static void remove(String appId, Map<String, ManagedProcess> processes) {
		stop(appId, processes);
		FileUtil.rd(new File("Apps/" + appId));
	}
	
	private static void shutdown(final ManagedProcess managedProcess) {
		// TODO
		if (managedProcess.niceShutdown != null) {
			managedProcess.niceShutdown.e();
			boolean waitUntil = ThreadUtil.waitUntil(new F0<Boolean>() {public Boolean e() {
				try {
					managedProcess.process.exitValue();
					return true;
				} catch (Exception e) {
					return false;
				}
			}}, 5000);
			if (waitUntil) {
				System.out.println("Old process of [" + managedProcess.appId + "] is found and shutdowned nicely");
				return;
			}
		}
		managedProcess.process.destroy();		
		ProcessUtil.waitFor(managedProcess.process);
		System.out.println("Old process of [" + managedProcess.appId + "] is found and killed");
	}
	

	protected static void stop(String appId, Map<String, ManagedProcess> processes) {
		ManagedProcess managedProcess = processes.remove(appId);
		if (managedProcess==null) {
			System.out.println("Old process of [" + appId + "] is not found");
			return;
		}
		if (ProcessUtil.isTerminated(managedProcess.process)) {
			System.out.println("Old process of [" + appId + "] is not active");
			return;
		}
		shutdown(managedProcess);
		
	}

	protected static void deploy(final String appId, byte[] content) {
		File tempFile = FileUtil.createTempFile();
		FileUtil.writeToFile(content, tempFile);
		
        ZipUtil.each(tempFile, new P2<String, InputStream>() {public void e(String path, InputStream content) {
			try {
				FileUtil.writeToFile(content, new File("Apps/" + appId + "/" + path));
			} catch (Exception e1) {
				System.out.println("Error writing file " + path + ": " + e1.getMessage());
//				throw new RuntimeException(e1);
			}
        }});
        tempFile.delete();
        if (!SystemUtil.isWindows() && new File("Apps/" + appId + "/run.sh").exists()) {
        	ProcessUtil.exec("chmod 777 run.sh", "Apps/" + appId);
        }
	}


	private static void startAll(Map<String, ManagedProcess> processes) {
		if (!new File("Apps").exists()) {
			return;
		}
		for (File file : new File("Apps").listFiles()) {
			if (!file.isDirectory()) {
				continue;
			}
			exec(file.getName(), processes);
		}
	}
	private static void stopAll(Map<String, ManagedProcess> processes) {
		for (String appId : new HashSet<String>(processes.keySet())) {
			shutdown(processes.remove(appId));
		}
	}

	protected static void restart(String appId,
			Map<String, ManagedProcess> processes) {
		System.out.println("Received command restart for app: " + appId);
		System.out.println("Terminating old app " + appId);
		stop(appId, processes);
		exec(appId, processes);
	}
	protected static void exec(String appId, Map<String, ManagedProcess> processes) {
		Process process = ProcessUtil.exec("run", "Apps/" + appId);
		processes.put(appId, new ManagedProcess( appId, process, getNiceShutdown(appId, process)));
		
		ProcessUtil.receiveOutput(process);
		
		System.out.println("Executed app " + appId + " successfully");
	}

	private static P0 getNiceShutdown(String appId, final Process process) {
		File configFile = new File("Apps/" + appId + "/AppHost.properties");
		if (!configFile.exists()) {
			return null;
		}
		String shutdownNice = PropertiesUtil.loadPropertiesFromFile(configFile).getProperty("shutdown.nice");
		if ("enter".equals(shutdownNice)) {
			return new P0() {public void e() {
				try {
					OutputStream out = process.getOutputStream();
					out.write('\n');
					out.flush();
				} catch (IOException e1) {
					throw new RuntimeException(e1.getMessage(), e1);
				}
			}};
		}
		return null;
	}

}
